1 package edu.jiangxin.apktoolbox.pdf.finder;
2
3 import edu.jiangxin.apktoolbox.pdf.PdfUtils;
4 import edu.jiangxin.apktoolbox.swing.extend.EasyPanel;
5 import edu.jiangxin.apktoolbox.swing.extend.FileListPanel;
6 import edu.jiangxin.apktoolbox.utils.Constants;
7 import edu.jiangxin.apktoolbox.utils.DateUtils;
8 import edu.jiangxin.apktoolbox.utils.FileUtils;
9 import edu.jiangxin.apktoolbox.utils.RevealFileUtils;
10
11 import javax.swing.*;
12 import javax.swing.table.DefaultTableModel;
13 import java.awt.*;
14 import java.awt.event.*;
15 import java.io.File;
16 import java.io.Serial;
17 import java.util.*;
18 import java.util.List;
19 import java.util.concurrent.ExecutorService;
20 import java.util.concurrent.Executors;
21 import java.util.concurrent.Future;
22 import java.util.concurrent.atomic.AtomicInteger;
23
24 public class PdfFinderPanel extends EasyPanel {
25
26 @Serial
27 private static final long serialVersionUID = 1L;
28
29 private JTabbedPane tabbedPane;
30
31 private JPanel mainPanel;
32
33 private FileListPanel fileListPanel;
34
35 private JRadioButton scannedRadioButton;
36
37 private JRadioButton encryptedRadioButton;
38
39 private JRadioButton nonOutlineRadioButton;
40
41 private JRadioButton hasAnnotationsRadioButton;
42
43 private JSpinner thresholdSpinner;
44
45 private JCheckBox isRecursiveSearched;
46
47 private JPanel resultPanel;
48
49 private JTable resultTable;
50
51 private DefaultTableModel resultTableModel;
52
53 private JButton searchButton;
54 private JButton cancelButton;
55
56 private JProgressBar progressBar;
57
58 private JMenuItem openDirMenuItem;
59
60 private JMenuItem copyFilesMenuItem;
61
62 private transient SearchThread searchThread;
63
64 private transient final List<File> resultFileList = new ArrayList<>();
65
66
67 @Override
68 public void initUI() {
69 tabbedPane = new JTabbedPane();
70 add(tabbedPane);
71
72 createMainPanel();
73 tabbedPane.addTab("Option", null, mainPanel, "Show Search Options");
74
75 createResultPanel();
76 tabbedPane.addTab("Result", null, resultPanel, "Show Search Result");
77 }
78
79 private void createMainPanel() {
80 mainPanel = new JPanel();
81 mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
82
83 fileListPanel = new FileListPanel();
84 fileListPanel.initialize();
85
86 JPanel checkOptionPanel = new JPanel();
87 checkOptionPanel.setLayout(new BoxLayout(checkOptionPanel, BoxLayout.X_AXIS));
88 checkOptionPanel.setBorder(BorderFactory.createTitledBorder("Check Options"));
89
90 ButtonGroup buttonGroup = new ButtonGroup();
91 ItemListener itemListener = new RadioButtonItemListener();
92
93 scannedRadioButton = new JRadioButton("查找扫描的PDF文件");
94 scannedRadioButton.setSelected(true);
95 scannedRadioButton.addItemListener(itemListener);
96 buttonGroup.add(scannedRadioButton);
97
98 encryptedRadioButton = new JRadioButton("查找加密的PDF文件");
99 encryptedRadioButton.addItemListener(itemListener);
100 buttonGroup.add(encryptedRadioButton);
101
102 nonOutlineRadioButton = new JRadioButton("查找没有目录的PDF文件");
103 nonOutlineRadioButton.addItemListener(itemListener);
104 buttonGroup.add(nonOutlineRadioButton);
105
106 hasAnnotationsRadioButton = new JRadioButton("查找有注释的PDF文件");
107 hasAnnotationsRadioButton.addItemListener(itemListener);
108 buttonGroup.add(hasAnnotationsRadioButton);
109
110 JPanel typePanel = new JPanel();
111 typePanel.setLayout(new FlowLayout(FlowLayout.LEFT,10,3));
112 typePanel.add(scannedRadioButton);
113 typePanel.add(encryptedRadioButton);
114 typePanel.add(nonOutlineRadioButton);
115 typePanel.add(hasAnnotationsRadioButton);
116
117 JLabel thresholdLabel = new JLabel("Threshold: ");
118 thresholdSpinner = new JSpinner();
119 thresholdSpinner.setModel(new SpinnerNumberModel(1, 0, 100, 1));
120
121 checkOptionPanel.add(typePanel);
122 checkOptionPanel.add(Box.createHorizontalStrut(Constants.DEFAULT_X_BORDER));
123 checkOptionPanel.add(thresholdLabel);
124 checkOptionPanel.add(Box.createHorizontalStrut(Constants.DEFAULT_X_BORDER));
125 checkOptionPanel.add(thresholdSpinner);
126 checkOptionPanel.add(Box.createHorizontalGlue());
127
128 JPanel searchOptionPanel = new JPanel();
129 searchOptionPanel.setLayout(new BoxLayout(searchOptionPanel, BoxLayout.X_AXIS));
130 searchOptionPanel.setBorder(BorderFactory.createTitledBorder("Search Options"));
131
132 isRecursiveSearched = new JCheckBox("Recursive");
133 isRecursiveSearched.setSelected(true);
134 searchOptionPanel.add(isRecursiveSearched);
135 searchOptionPanel.add(Box.createHorizontalGlue());
136
137 JPanel operationPanel = new JPanel();
138 operationPanel.setLayout(new BoxLayout(operationPanel, BoxLayout.X_AXIS));
139 operationPanel.setBorder(BorderFactory.createTitledBorder("Operations"));
140
141 JPanel buttonPanel = new JPanel();
142 buttonPanel.setLayout(new BoxLayout(buttonPanel, BoxLayout.X_AXIS));
143
144 searchButton = new JButton("Search");
145 cancelButton = new JButton("Cancel");
146 cancelButton.setEnabled(false);
147 searchButton.addActionListener(new OperationButtonActionListener());
148 cancelButton.addActionListener(new OperationButtonActionListener());
149 operationPanel.add(searchButton);
150 operationPanel.add(Box.createHorizontalStrut(Constants.DEFAULT_X_BORDER));
151 operationPanel.add(Box.createHorizontalStrut(Constants.DEFAULT_X_BORDER));
152 operationPanel.add(cancelButton);
153 operationPanel.add(Box.createHorizontalGlue());
154
155 progressBar = new JProgressBar();
156 progressBar.setStringPainted(true);
157 progressBar.setString("Ready");
158
159 mainPanel.add(fileListPanel);
160 mainPanel.add(Box.createVerticalStrut(Constants.DEFAULT_Y_BORDER));
161 mainPanel.add(checkOptionPanel);
162 mainPanel.add(Box.createVerticalStrut(Constants.DEFAULT_Y_BORDER));
163 mainPanel.add(searchOptionPanel);
164 mainPanel.add(Box.createVerticalStrut(Constants.DEFAULT_Y_BORDER));
165 mainPanel.add(operationPanel);
166 mainPanel.add(Box.createVerticalStrut(Constants.DEFAULT_Y_BORDER));
167 mainPanel.add(progressBar);
168 }
169
170 private void createResultPanel() {
171 resultPanel = new JPanel();
172 resultPanel.setLayout(new BoxLayout(resultPanel, BoxLayout.Y_AXIS));
173
174 resultTableModel = new PdfFilesTableModel(new Vector<>(), PdfFilesConstants.COLUMN_NAMES);
175 resultTable = new JTable(resultTableModel);
176
177 resultTable.setDefaultRenderer(Vector.class, new PdfFilesTableCellRenderer());
178
179 for (int i = 0; i < resultTable.getColumnCount(); i++) {
180 resultTable.getColumn(resultTable.getColumnName(i)).setCellRenderer(new PdfFilesTableCellRenderer());
181 }
182
183 resultTable.addMouseListener(new MyMouseListener());
184
185 resultTable.setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION);
186
187 JScrollPane scrollPane = new JScrollPane(resultTable);
188 resultPanel.add(scrollPane);
189 }
190
191 private void processFile(File file) {
192 if (scannedRadioButton.isSelected()) {
193 int threshold = (Integer) thresholdSpinner.getValue();
194 if (PdfUtils.isScannedPdf(file, threshold)) {
195 resultFileList.add(file);
196 }
197 } else if (encryptedRadioButton.isSelected()) {
198 if (PdfUtils.isEncryptedPdf(file)) {
199 resultFileList.add(file);
200 }
201 } else if (nonOutlineRadioButton.isSelected()) {
202 if (PdfUtils.isNonOutlinePdf(file)) {
203 resultFileList.add(file);
204 }
205 } else if (hasAnnotationsRadioButton.isSelected()) {
206 if (PdfUtils.hasAnnotations(file)) {
207 resultFileList.add(file);
208 }
209 } else {
210 logger.error("Invalid option selected");
211 }
212 }
213
214 class MyMouseListener extends MouseAdapter {
215 @Override
216 public void mouseReleased(MouseEvent e) {
217 super.mouseReleased(e);
218 int r = resultTable.rowAtPoint(e.getPoint());
219 if (r >= 0 && r < resultTable.getRowCount()) {
220 if (!resultTable.isRowSelected(r)) {
221 resultTable.setRowSelectionInterval(r, r);
222 }
223 } else {
224 resultTable.clearSelection();
225 }
226 int[] rowsIndex = resultTable.getSelectedRows();
227 if (rowsIndex == null || rowsIndex.length == 0) {
228 return;
229 }
230 if (e.isPopupTrigger() && e.getComponent() instanceof JTable) {
231 JPopupMenu popupmenu = new JPopupMenu();
232 MyMenuActionListener menuActionListener = new MyMenuActionListener();
233
234 if (rowsIndex.length == 1) {
235 openDirMenuItem = new JMenuItem("Open parent folder of this file");
236 openDirMenuItem.addActionListener(menuActionListener);
237 popupmenu.add(openDirMenuItem);
238 popupmenu.addSeparator();
239 }
240
241 copyFilesMenuItem = new JMenuItem("Copy selected files to...");
242 copyFilesMenuItem.addActionListener(menuActionListener);
243 popupmenu.add(copyFilesMenuItem);
244
245 popupmenu.show(e.getComponent(), e.getX(), e.getY());
246 }
247 }
248 }
249
250 class MyMenuActionListener implements ActionListener {
251 @Override
252 public void actionPerformed(ActionEvent actionEvent) {
253 Object source = actionEvent.getSource();
254 if (source.equals(openDirMenuItem)) {
255 onOpenDir();
256 } else if (source.equals(copyFilesMenuItem)) {
257 onCopyFiles();
258 } else {
259 logger.error("invalid source");
260 }
261 }
262
263 private void onOpenDir() {
264 int rowIndex = resultTable.getSelectedRow();
265 String parentPath = resultTableModel.getValueAt(rowIndex, resultTable.getColumn(PdfFilesConstants.COLUMN_NAME_FILE_PARENT).getModelIndex()).toString();
266 File parent = new File(parentPath);
267 RevealFileUtils.revealDirectory(parent);
268 }
269
270 private void onCopyFiles() {
271 int[] selectedRows = resultTable.getSelectedRows();
272 if (selectedRows.length == 0) {
273 JOptionPane.showMessageDialog(PdfFinderPanel.this, "No rows selected", "Error", JOptionPane.ERROR_MESSAGE);
274 return;
275 }
276 List<File> filesToCopy = new ArrayList<>();
277 for (int rowIndex : selectedRows) {
278 String filePath = resultTableModel.getValueAt(rowIndex, resultTable.getColumn(PdfFilesConstants.COLUMN_NAME_FILE_NAME).getModelIndex()).toString();
279 String parentPath = resultTableModel.getValueAt(rowIndex, resultTable.getColumn(PdfFilesConstants.COLUMN_NAME_FILE_PARENT).getModelIndex()).toString();
280 File file = new File(parentPath, filePath);
281 filesToCopy.add(file);
282 }
283 JFileChooser fileChooser = new JFileChooser();
284 fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
285 fileChooser.setDialogTitle("Select Target Directory");
286 int returnValue = fileChooser.showOpenDialog(PdfFinderPanel.this);
287 if (returnValue != JFileChooser.APPROVE_OPTION) {
288 return;
289 }
290 File targetDir = fileChooser.getSelectedFile();
291 for (File file : filesToCopy) {
292 try {
293 org.apache.commons.io.FileUtils.copyFileToDirectory(file, targetDir);
294 } catch (Exception e) {
295 logger.error("Copy file failed: " + file.getAbsolutePath(), e);
296 }
297 }
298 }
299
300
301 }
302
303 class OperationButtonActionListener implements ActionListener {
304 @Override
305 public void actionPerformed(ActionEvent e) {
306 Object source = e.getSource();
307 if (source.equals(searchButton)) {
308 searchButton.setEnabled(false);
309 cancelButton.setEnabled(true);
310 searchThread = new SearchThread(isRecursiveSearched.isSelected());
311 searchThread.start();
312 } else if (source.equals(cancelButton)) {
313 searchButton.setEnabled(true);
314 cancelButton.setEnabled(false);
315 if (searchThread.isAlive()) {
316 searchThread.interrupt();
317 searchThread.executorService.shutdownNow();
318 }
319 }
320
321 }
322 }
323
324 private void showResult() {
325 SwingUtilities.invokeLater(() -> {
326 int index = 0;
327 for (File file : resultFileList) {
328 index++;
329 Vector<Object> rowData = getRowVector(index, file);
330 resultTableModel.addRow(rowData);
331 }
332 tabbedPane.setSelectedIndex(1);
333 });
334 }
335
336 private Vector<Object> getRowVector(int index, File file) {
337 Vector<Object> rowData = new Vector<>();
338 rowData.add(index);
339 rowData.add(file.getParent());
340 rowData.add(file.getName());
341 rowData.add(FileUtils.sizeOfInHumanFormat(file));
342 rowData.add(DateUtils.millisecondToHumanFormat(file.lastModified()));
343 return rowData;
344 }
345
346 class SearchThread extends Thread {
347 public final ExecutorService executorService;
348 private final AtomicInteger processedFiles = new AtomicInteger(0);
349 private int totalFiles = 0;
350 private final boolean isRecursiveSearched;
351
352 public SearchThread(boolean isRecursiveSearched) {
353 super();
354 this.isRecursiveSearched = isRecursiveSearched;
355 this.executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
356
357 SwingUtilities.invokeLater(() -> {
358 progressBar.setValue(0);
359 progressBar.setString("Starting search...");
360 });
361 }
362
363 @Override
364 public void run() {
365 try {
366 resultFileList.clear();
367 SwingUtilities.invokeLater(() -> resultTableModel.setRowCount(0));
368
369 List<File> fileList = fileListPanel.getFileList();
370 Set<File> fileSet = new TreeSet<>();
371 String[] extensions = new String[]{"pdf", "PDF"};
372 for (File file : fileList) {
373 fileSet.addAll(FileUtils.listFiles(file, extensions, isRecursiveSearched));
374 }
375
376 List<Future<?>> futures = new ArrayList<>();
377 totalFiles = fileSet.size();
378 updateProgress();
379
380 for (File file : fileSet) {
381 if (currentThread().isInterrupted()) {
382 return;
383 }
384 futures.add(executorService.submit(() -> {
385 if (currentThread().isInterrupted()) {
386 return null;
387 }
388 processFile(file);
389 incrementProcessedFiles();
390 return null;
391 }));
392 }
393
394
395 for (Future<?> future : futures) {
396 try {
397 future.get();
398 } catch (InterruptedException e) {
399 logger.error("Search interrupted", e);
400 currentThread().interrupt();
401 return;
402 }
403 }
404
405 showResult();
406 } catch (Exception e) {
407 logger.error("Search failed", e);
408 SwingUtilities.invokeLater(() -> progressBar.setString("Search failed"));
409 } finally {
410 executorService.shutdown();
411 SwingUtilities.invokeLater(() -> {
412 searchButton.setEnabled(true);
413 cancelButton.setEnabled(false);
414 });
415 }
416 }
417
418 private void incrementProcessedFiles() {
419 processedFiles.incrementAndGet();
420 updateProgress();
421 }
422
423 private void updateProgress() {
424 if (totalFiles > 0) {
425 SwingUtilities.invokeLater(() -> {
426 int processed = processedFiles.get();
427 int percentage = (int) (processed * 100.0 / totalFiles);
428 progressBar.setValue(percentage);
429 progressBar.setString(String.format("Processing: %d/%d files (%d%%)", processed, totalFiles, percentage));
430 });
431 }
432 }
433 }
434
435 class RadioButtonItemListener implements ItemListener {
436 @Override
437 public void itemStateChanged(ItemEvent e) {
438 thresholdSpinner.setEnabled(scannedRadioButton.isSelected());
439 }
440 }
441 }